// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace GSpec.Framework
{
	using System;
	using System.Collections.Generic;
	using System.Diagnostics;

	using Gallio.Common.Collections;
	using Gallio.Common.Reflection;
	using Gallio.Framework;
	using Gallio.Framework.Data;
	using Gallio.Framework.Pattern;
	using Gallio.Model;

	[AttributeUsage(PatternAttributeTargets.TestMethod, AllowMultiple = false, Inherited = true)]
	public class ThenAttribute : PatternAttribute
	{
		private static string ExceptionDataParameterName = "ExceptionData";

		/// <summary>
		/// Gets or sets a number that defines an ordering for the test with respect to its siblings.
		/// </summary>
		/// <remarks>
		/// <para>
		/// Unless compelled otherwise by test dependencies, tests with a lower order number than
		/// their siblings will run before those siblings and tests with the same order number
		/// as their siblings with run in an arbitrary sequence with respect to those siblings.
		/// </para>
		/// </remarks>
		/// <value>The test execution order with respect to siblings, initially zero.</value>
		public int Order { get; set; }

		/// <inheritdoc />
		public override bool IsPrimary
		{
			get { return true; }
		}

		/// <inheritdoc />
		public override IList<TestPart> GetTestParts(IPatternEvaluator evaluator, ICodeElementInfo codeElement)
		{
			return new[] { new TestPart() { IsTestCase = true } };
		}

		/// <inheritdoc />
		public override void Consume(IPatternScope containingScope, ICodeElementInfo codeElement, bool skipChildren)
		{
			IMethodInfo method = codeElement as IMethodInfo;
			Validate(containingScope, method);

			IPatternScope methodScope = containingScope.CreateChildTestScope(method.Name, method);
			methodScope.TestBuilder.Kind = "Observation";
			methodScope.TestBuilder.IsTestCase = true;
			methodScope.TestBuilder.Order = Order;
			//methodScope.TestBuilder.TimeoutFunc = () => TestAssemblyExecutionParameters.DefaultTestCaseTimeout;

			InitializeTest(methodScope, method);
			SetTestSemantics(methodScope.TestBuilder, method);

			methodScope.TestBuilder.ApplyDeferredActions();
		}

		/// <summary>
		/// Verifies that the attribute is being used correctly.
		/// </summary>
		/// <param name="containingScope">The containing scope.</param>
		/// <param name="method">The method.</param>
		/// <exception cref="PatternUsageErrorException">Thrown if the attribute is being used incorrectly.</exception>
		protected virtual void Validate(IPatternScope containingScope, IMethodInfo method)
		{
			if (!containingScope.CanAddChildTest || method == null)
				ThrowUsageErrorException("This attribute can only be used on a test method within a test type.");
			if (method.IsAbstract)
				ThrowUsageErrorException("This attribute cannot be used on an abstract method.");
		}

		/// <summary>
		/// Initializes a test for a method after it has been added to the test model.
		/// </summary>
		/// <param name="methodScope">The method scope.</param>
		/// <param name="method">The method.</param>
		protected virtual void InitializeTest(IPatternScope methodScope, IMethodInfo method)
		{
			string xmlDocumentation = method.GetXmlDocumentation();
			if (xmlDocumentation != null)
				methodScope.TestBuilder.AddMetadata(MetadataKeys.XmlDocumentation, xmlDocumentation);

			methodScope.Process(method);

			if (method.IsGenericMethodDefinition)
			{
				ThrowUsageErrorException("Generic methods are not supported for observations");
			}

			switch (method.Parameters.Count)
			{
				case 0:
					break;

				case 1:
					IParameterInfo exceptionParameter = method.Parameters[0];
					Type parameterType = exceptionParameter.Resolve(false).ParameterType;

					if (parameterType != typeof(Exception))
						ThrowUsageErrorException("Only the exception can be set as paramater for the observation");

					methodScope.TestBuilder.AddMetadata(MetaKeys.WhenException, parameterType.FullName);

					break;

				default:
					ThrowUsageErrorException("Only the exception can be set as paramater for the observation");

					break;
			}
		}

		/// <summary>
		/// Applies semantic actions to a test to estalish its runtime behavior.
		/// </summary>
		/// <remarks>
		/// <para>
		/// This method is called after <see cref="InitializeTest" />.
		/// </para>
		/// <para>
		/// The default behavior for a <see cref="TestMethodPatternAttribute" />
		/// is to configure the test actions as follows:
		/// <list type="bullet">
		/// <item><see cref="PatternTestInstanceActions.BeforeTestInstanceChain" />: Set the
		/// test step name, <see cref="PatternTestInstanceState.TestMethod" /> and
		/// <see cref="PatternTestInstanceState.TestArguments" /> based on any values bound
		/// to the test method's generic parameter and method parameter slots.</item>
		/// <item><see cref="PatternTestInstanceActions.ExecuteTestInstanceChain" />: Invoke the method.</item>
		/// </list>
		/// </para>
		/// <para>
		/// You can override this method to change the semantics as required.
		/// </para>
		/// </remarks>
		/// <param name="testBuilder">The test builder.</param>
		/// <param name="method">The test method.</param>
		protected virtual void SetTestSemantics(ITestBuilder testBuilder, IMethodInfo method)
		{
			testBuilder.TestInstanceActions.ExecuteTestInstanceChain.After(
				state => state.InvokeFixtureMethod(method, GetArguments(state, method)));
		}

		private static IEnumerable<KeyValuePair<ISlotInfo, object>> GetArguments(PatternTestInstanceState state, IMethodInfo method)
		{
			if (method.Parameters.Count == 0) return EmptyArray<KeyValuePair<ISlotInfo, object>>.Instance;

			Exception exception = TestContext.CurrentContext.Data.GetValue(WhenAttribute.WhenExceptionKey);
			return new[]
			       	{
			       		new KeyValuePair<ISlotInfo, object>(
									method.Parameters[0], exception), 
							};
		}
	}
}